SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
26.3 KB · 417 lines tsx
Raw Blame History
1import { WatchButton } from "@/components/intelligence/watch-button";2import type { EventType, ProjectDetail } from "@dci/core";3import { partialDateSortKey } from "@dci/core";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import type { ReactNode } from "react";8import { Breadcrumbs } from "@/components/entity/breadcrumbs";9import { EntityMap } from "@/components/entity/entity-map";10import { JsonLd } from "@/components/entity/json-ld";11import { ExtLink, KeyValueList } from "@/components/entity/kv";12import { ProvenanceTable, WithProvenance } from "@/components/entity/provenance";13import { SectionNav } from "@/components/entity/section-nav";14import { SourcesFooter } from "@/components/entity/sources-footer";15import { EventList } from "@/components/event-list";16import { ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";17import { ProjectStages, ProjectVelocity } from "@/components/project-stages";18import { AiMark, Badge, ConfidenceBadge, EstimateMark, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge } from "@/components/ui/badge";19import { Page, Section } from "@/components/ui/card";20import { CountryFlag } from "@/components/ui/country-flag";21import { ErrorState, OfflineNotice } from "@/components/ui/states";22import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";23import { countryName, fmtDate, fmtInt, fmtMw, fmtUsd, placeLine, splitMw } from "@/lib/format";24import { fmtPartialDateSafe } from "@/lib/format-project";25import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, isSiteScopeLabel } from "@/lib/labels";26import { routes } from "@/lib/routes";27import { absoluteUrl, compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";28import { getProject } from "./data";2930export const revalidate = 300;3132type Props = { params: Promise<{ slug: string }> };3334function describe(p: ProjectDetail): string {35  if (p.description) return p.description;36  const bits = [`${p.name}: ${STATUS_LABEL[p.status].toLowerCase()} data center project`];37  if (p.operator) bits.push(`by ${p.operator.name}`);38  const place = placeLine(p);39  if (place !== "—") bits.push(`in ${place}`);40  if (p.plannedMw !== null && siteScoped(p.capacityScope)) bits.push(`— ≈ ${fmtMw(p.plannedMw)} planned (published figure)`);41  if (p.investmentUsd !== null && siteScoped(p.investmentScope)) bits.push(`, ${fmtUsd(p.investmentUsd)} announced`);42  if (p.expectedOpening) bits.push(`, expected ${fmtPartialDateSafe(p.expectedOpening)}`);43  return `${bits.join(" ")}. Lifecycle stages, timeline, claims with scope and provenance.`;44}4546/** A header figure may only stand for the project when its scope is a site scope (or unknown-but-assigned legacy). */47function siteScoped(scope: string | null | undefined): boolean {48  return scope === null || scope === undefined || isSiteScopeLabel(scope);49}5051export async function generateMetadata({ params }: Props): Promise<Metadata> {52  const { slug } = await params;53  const r = await getProject(slug);54  if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Project" };55  return pageMetadata({ title: r.data.name, description: describe(r.data), path: routes.project(r.data.slug) });56}5758function humanType(t: string): string {59  return EVENT_LABEL[t as EventType] ?? t.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());60}6162function toneFor(t: string): TimelineEntry["tone"] {63  switch (t) {64    case "construction_started":65    case "phase_announced":66      return "warn";67    case "facility_opened":68    case "cloud_region_launched":69      return "ok";70    case "project_announced":71    case "expansion_announced":72    case "investment_announced":73    case "power_agreement":74      return "accent";75    case "closure":76    case "incident":77    case "project_cancelled":78      return "danger";79    default:80      return "neutral";81  }82}8384function Figure({ label, children, note }: { label: ReactNode; children: ReactNode; note?: ReactNode }) {85  return (86    <div className="min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0">87      <div className="label-caps mb-1 truncate">{label}</div>88      <div className="min-w-0">{children}</div>89      {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}90    </div>91  );92}9394const PROJECT_CLASS_LABEL: Record<string, string> = {95  NEW_BUILD: "New build",96  EXPANSION: "Expansion",97  CONSTRUCTION_START: "Construction start",98  PERMIT: "Permit",99  LAND_ACQUISITION: "Land acquisition",100  GRID_CONNECTION: "Grid connection",101  POWER_AGREEMENT: "Power agreement",102  FINANCING: "Financing",103  ACQUISITION: "Acquisition",104  PARTNERSHIP: "Partnership",105  CUSTOMER_AGREEMENT: "Customer agreement",106};107108export default async function ProjectPage({ params }: Props) {109  const { slug } = await params;110  const r = await getProject(slug);111  if (!r.ok && r.status === 404) notFound();112  if (!r.ok) {113    return (114      <Page className="py-6">115        <OfflineNotice detail={r.error} className="mb-4" />116        <ErrorState />117      </Page>118    );119  }120  const p = r.data;121  const base = routes.project(p.slug);122  const place = placeLine(p);123  const hasGeo = p.lat !== null && p.lng !== null;124  const precision = p.geoPrecision ?? (hasGeo ? "city" : "unknown");125  const claims = p.claims ?? [];126  const history = p.history ?? [];127  const mwSite = siteScoped(p.capacityScope);128  const usdSite = siteScoped(p.investmentScope);129  const dq = p.dataQuality;130131  const timeline: TimelineEntry[] = [...p.timeline]132    .sort((a, b) => partialDateSortKey(a.date.length > 10 ? a.date.slice(0, 10) : a.date) - partialDateSortKey(b.date.length > 10 ? b.date.slice(0, 10) : b.date))133    .map((t) => ({ date: t.date, title: humanType(t.type), description: t.description, meta: t.sourceName ? <span>{t.sourceName}</span> : undefined, href: t.url, tone: toneFor(t.type) }));134  for (const s of p.statusHistory) timeline.push({ date: s.date, title: `${s.from ? `${STATUS_LABEL[s.from]} → ` : ""}${STATUS_LABEL[s.to]}`, href: s.url, tone: s.to === "under_construction" ? "warn" : s.to === "operational" ? "ok" : s.to === "cancelled" || s.to === "delayed" ? "danger" : "neutral" });135  if (p.expectedOpening) timeline.push({ date: p.expectedOpening, title: <span className="text-ink-2">Expected opening</span>, description: p.phaseCount ? `phase 1 of ${fmtInt(p.phaseCount)} — expected, as published by the source` : "expected, as published by the source", tone: "neutral" });136  timeline.sort((a, b) => partialDateSortKey(a.date.slice(0, 10)) - partialDateSortKey(b.date.slice(0, 10)));137138  const sections = [139    { id: "lifecycle", label: "Lifecycle" },140    { id: "figures", label: "Figures & claims" },141    { id: "timeline", label: "Timeline", count: timeline.length },142    { id: "about", label: "About", count: p.description || p.sourceUrl || p.facility || p.campus ? 1 : 0 },143    { id: "location", label: "Location" },144    { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },145    { id: "related", label: "Related events", count: p.relatedEvents.length + p.events.length },146    { id: "provenance", label: "Source evidence", count: p.provenance.length },147    { id: "quality", label: "Data quality" },148  ];149150  const jsonLd = compactJsonLd({151    "@context": "https://schema.org",152    "@type": "Project",153    name: p.name,154    description: p.description ?? undefined,155    url: absoluteUrl(base),156    sameAs: p.sourceUrl ?? undefined,157    foundingDate: p.announcedOn ?? undefined,158    parentOrganization: p.operator ? { "@type": "Organization", name: p.operator.name, url: absoluteUrl(routes.operator(p.operator.slug)) } : undefined,159    location: place !== "—" || hasGeo ? compactJsonLd({ "@type": "Place", name: place !== "—" ? place : undefined, address: p.countryIso2 ? { "@type": "PostalAddress", addressLocality: p.city ?? undefined, addressRegion: p.regionName ?? undefined, addressCountry: p.countryIso2 } : undefined, geo: hasGeo && precision !== "unknown" ? { "@type": "GeoCoordinates", latitude: p.lat, longitude: p.lng } : undefined }) : undefined,160    additionalProperty: [161      { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[p.status] },162      p.plannedMw !== null && mwSite ? { "@type": "PropertyValue", name: "Planned capacity", value: p.plannedMw, unitText: "MW" } : null,163      { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[precision] },164    ].filter(Boolean),165  });166167  const mwSplit = splitMw(p.plannedMw);168169  return (170    <EvidenceProvider data={{ entityName: p.name, entityKind: "project", claims, provenance: p.provenance, history }}>171      <Page>172        <Breadcrumbs items={[{ name: "Projects", href: routes.projects() }, { name: p.name, href: base }]} className="pt-3" />173        <JsonLd data={jsonLd} />174175        <header className="py-5 sm:py-7">176          <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">177            <span>Project</span>178            {p.projectClass && PROJECT_CLASS_LABEL[p.projectClass] && (179              <>180                <span aria-hidden className="text-ink-4">·</span>181                <span>{PROJECT_CLASS_LABEL[p.projectClass]}</span>182              </>183            )}184            {p.operator && (185              <>186                <span aria-hidden className="text-ink-4">·</span>187                <Link href={routes.operator(p.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">188                  {p.operator.name}189                </Link>190              </>191            )}192          </div>193          <div className="flex flex-wrap items-start justify-between gap-2">194            <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{p.name}</h1>195            <WatchButton entityType="project" slug={p.slug} entityId={p.id} name={p.name} />196          </div>197          <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">198            {!p.operator && <span className="text-ink-3">Operator not published</span>}199            {p.developer && p.developer.id !== p.operator?.id && (200              <span>201                developed by{" "}202                <Link href={routes.operator(p.developer.slug)} className="text-ink hover:text-accent hover:underline">203                  {p.developer.name}204                </Link>205              </span>206            )}207            {p.tenant && (208              <span>209                for{" "}210                <Link href={routes.operator(p.tenant.slug)} className="text-ink hover:text-accent hover:underline">211                  {p.tenant.name}212                </Link>213              </span>214            )}215            {(place !== "—" || p.metro) && (216              <span className="inline-flex items-center gap-1.5">217                <CountryFlag iso2={p.countryIso2} size={12} />218                {p.metro && (219                  <Link href={routes.market(p.metro.slug)} className="hover:text-ink hover:underline">220                    {p.metro.name} market221                  </Link>222                )}223                {p.metro && place !== "—" && <span className="text-ink-4">·</span>}224                {place !== "—" && <span>{place}</span>}225              </span>226            )}227          </div>228          <div className="mt-3 flex flex-wrap items-center gap-1.5">229            {p.isAi && <AiMark evidence={p.aiEvidence} size="sm" />}230            {p.evidenceLevel && (231              <Badge tone={p.evidenceLevel === "strong" ? "ok" : p.evidenceLevel === "weak" ? "warn" : "outline"} size="sm" title="Creation evidence: name or operator + location + development verb">232                {p.evidenceLevel} evidence233              </Badge>234            )}235            {p.phaseCount !== null && (236              <Badge tone="outline" size="sm">237                {fmtInt(p.phaseCount)} phases238              </Badge>239            )}240          </div>241        </header>242243        {/* header figures */}244        <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*]:border-hair sm:grid-cols-3 sm:[&>*:nth-child(n+4)]:border-t sm:[&>*:nth-child(3n+1)]:pl-0 sm:[&>*:not(:nth-child(3n+1))]:border-l sm:[&>*]:border-hair lg:grid-cols-6 lg:[&>*:nth-child(n+4)]:border-t-0 lg:[&>*:nth-child(3n+1)]:pl-4 lg:[&>*:first-child]:pl-0 lg:[&>*+*]:border-l">245          <Figure label="Stage">246            <Evidence field="status" display={STATUS_LABEL[p.status]}>247              <StatusBadge status={p.status} />248            </Evidence>249          </Figure>250          <Figure label="Planned capacity">251            {p.plannedMw !== null && mwSite && mwSplit ? (252              <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display={`≈ ${fmtMw(p.plannedMw)}`} block>253                <div className="flex items-baseline gap-1 leading-none">254                  <EstimateMark className="text-[14px]" />255                  <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{mwSplit.value}</span>256                  <span className="text-[12px] font-medium text-ink-3">{mwSplit.unit}</span>257                </div>258                <div className="mt-1 flex flex-wrap items-center gap-1.5">259                  <SemanticsLabel semantics={p.capacitySemantics ?? "planned_power_mw"} />260                  {p.capacityScope && <ScopeBadge scope={p.capacityScope} />}261                </div>262              </Evidence>263            ) : (264              <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display="—" block>265                <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>266                <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.plannedMw !== null ? `${fmtMw(p.plannedMw)} is a ${p.capacityScope}-level figure — not this site's (see claims)` : "Not published by any source"}</div>267              </Evidence>268            )}269          </Figure>270          <Figure label="Investment">271            {p.investmentUsd !== null && usdSite ? (272              <Evidence field="investmentUsd" fields={["investment_usd"]} display={fmtUsd(p.investmentUsd)} block>273                <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink sm:text-[26px]">{fmtUsd(p.investmentUsd)}</div>274                <div className="mt-1 flex flex-wrap items-center gap-1.5">275                  <SemanticsLabel semantics={p.investmentSemantics ?? "project_investment_usd"} />276                  {p.investmentScope && <ScopeBadge scope={p.investmentScope} />}277                  {p.investmentCurrency && p.investmentCurrency !== "USD" && p.investmentOriginal && <span className="figure text-[11px] text-ink-3">{fmtInt(p.investmentOriginal)} {p.investmentCurrency}</span>}278                </div>279              </Evidence>280            ) : (281              <Evidence field="investmentUsd" fields={["investment_usd"]} display="—" block>282                <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>283                <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.investmentUsd !== null ? `${fmtUsd(p.investmentUsd)} is a ${p.investmentScope}-level figure — not this site's (see claims)` : "Not disclosed"}</div>284              </Evidence>285            )}286          </Figure>287          <Figure label="Expected opening" note={p.expectedOpening ? "as published" : "not published"}>288            <Evidence field="expectedOpening" fields={["expected_opening"]} display={p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"} block>289              <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.expectedOpening ? "text-ink" : "text-ink-4"}`}>{p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"}</div>290            </Evidence>291          </Figure>292          <Figure label="Announced" note={p.announcedOn ? undefined : "date not published"}>293            <Evidence field="announcedOn" fields={["announced_on"]} display={p.announcedOn ? fmtDate(p.announcedOn) : "—"} block>294              <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.announcedOn ? "text-ink" : "text-ink-4"}`}>{p.announcedOn ? fmtDate(p.announcedOn) : "—"}</div>295            </Evidence>296          </Figure>297          <Figure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>298            <ConfidenceBadge level={p.confidence} short={false} />299          </Figure>300        </div>301        <p className="mt-2 text-[11px] text-ink-4">Planned MW and investment are published figures (≈ never imputed). A company-wide or portfolio figure is never shown as this project&apos;s own; it is listed under “Figures not assigned to this site”. Click a figure for its evidence.</p>302303        <SectionNav items={sections} className="mt-4" />304305        <div className="flex flex-col gap-9 pt-6">306          <Section id="lifecycle" title="Lifecycle" className="scroll-mt-28" description="Rumored → proposed → announced → permitting → approved → construction → opened. Dates come from the announcement that moved the stage; delayed / cancelled are side branches.">307            <ProjectStages stages={p.stages} />308            <div className="mt-6">309              <div className="label-caps mb-2">Velocity</div>310              <ProjectVelocity v={p.velocityDays} />311            </div>312          </Section>313314          <Section id="figures" title="Figures & claims" className="scroll-mt-28" description="Every capacity and investment claim extracted for this project with its scope. Only site-scoped claims can become the project's figures.">315            <div className="flex flex-col gap-5">316              <div>317                <div className="label-caps mb-2">Capacity claims</div>318                <ClaimsTable claims={claims} kind="capacity" emptyNote={p.plannedMw !== null ? "No sentence-level capacity claim stored yet — the planned figure comes from the source fields listed under Source evidence." : "No source has published a capacity figure for this project."} />319              </div>320              <div>321                <div className="label-caps mb-2">Investment claims</div>322                <ClaimsTable claims={claims} kind="investment" emptyNote={p.investmentUsd !== null ? "No sentence-level investment claim stored yet — the figure comes from the source fields listed under Source evidence." : "No investment figure disclosed."} />323              </div>324              {((p.plannedMw !== null && !mwSite) || (p.investmentUsd !== null && !usdSite)) && (325                <div>326                  <div className="mb-1.5 flex flex-wrap items-baseline gap-x-2">327                    <h3 className="text-[13px] font-semibold text-ink">Figures not assigned to this site</h3>328                    <span className="text-[11.5px] text-ink-3">scope is wider than the project — shown as context only</span>329                  </div>330                  <KeyValueList331                    items={[332                      { label: "Planned MW", value: p.plannedMw !== null && !mwSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">≈ {fmtMw(p.plannedMw)}</span><ScopeBadge scope={p.capacityScope} /><SemanticsLabel semantics={p.capacitySemantics} /></span> : null },333                      { label: "Investment", value: p.investmentUsd !== null && !usdSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">{fmtUsd(p.investmentUsd)}</span><ScopeBadge scope={p.investmentScope} /><SemanticsLabel semantics={p.investmentSemantics} /></span> : null },334                    ]}335                  />336                </div>337              )}338            </div>339          </Section>340341          <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and status transitions from the sources indexed; partial dates (year, quarter) are shown as published.">342            {timeline.length ? <TimelineList items={timeline} /> : <p className="text-[12.5px] text-ink-3">No dated milestone has been published for this project yet.</p>}343          </Section>344345          <Section id="about" title="About" className="scroll-mt-28">346            {p.description ? <p className="max-w-3xl text-[14px] leading-relaxed text-ink-2">{p.description}</p> : <p className="text-[12.5px] text-ink-3">No description published.</p>}347            <KeyValueList348              className="mt-3"349              columns={2}350              items={[351                { label: "Operator", value: p.operator ? <Link href={routes.operator(p.operator.slug)} className="hover:text-accent hover:underline">{p.operator.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },352                { label: "Developer", value: p.developer ? <Link href={routes.operator(p.developer.slug)} className="hover:text-accent hover:underline">{p.developer.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },353                { label: "Tenant", value: p.tenant ? <Link href={routes.operator(p.tenant.slug)} className="hover:text-accent hover:underline">{p.tenant.name}</Link> : null },354                { label: "Facility", value: p.facility ? <Link href={routes.facility(p.facility.slug)} className="hover:text-accent hover:underline">{p.facility.name}</Link> : null },355                { label: "Campus", value: p.campus ? <Link href={routes.facility(p.campus.slug)} className="hover:text-accent hover:underline">{p.campus.name}</Link> : null },356                { label: "Site", value: p.acreage !== null ? <span className="figure">{fmtInt(p.acreage)} acres</span> : null },357                { label: "Primary source", value: p.sourceUrl ? <ExtLink href={p.sourceUrl} /> : null },358                { label: "Last update", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(p.lastUpdate)}</span> },359              ]}360            />361          </Section>362363          <Section id="location" title="Location" className="scroll-mt-28" description={hasGeo ? `${PRECISION_LABEL[precision]} — the ring marks the area published by the sources, not a parcel.` : "Location as published; no coordinates yet."} aside={<PrecisionBadge precision={precision} short={false} size="xs" />}>364            <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">365              <KeyValueList366                items={[367                  { label: "City", value: p.city },368                  { label: "Region", value: p.regionName },369                  { label: "Country", value: p.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={p.countryIso2} size={12} />{countryName(p.countryIso2)}</span> : null },370                  { label: "Market", value: p.metro ? <Link href={routes.market(p.metro.slug)} className="hover:text-accent hover:underline">{p.metro.name}</Link> : null },371                  { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure">{p.lat!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}, {p.lng!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },372                  { label: "Precision", value: <WithProvenance items={p.provenance} fields={["geoPrecision", "geo_precision"]}><PrecisionBadge precision={precision} short={false} size="xs" /></WithProvenance>, keepEmpty: true },373                ]}374              />375              {hasGeo ? <EntityMap center={{ lat: p.lat!, lng: p.lng! }} precision={precision} filters={p.countryIso2 ? { country: p.countryIso2 } : undefined} className="h-[220px] sm:h-[280px]" /> : <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>}376            </div>377          </Section>378379          <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance from the published (often city-level) point. Proximity never implies a connection.">380            {hasGeo ? <NearbyInfrastructurePanel initial={p.nearbyInfrastructure} center={{ lat: p.lat!, lng: p.lng! }} /> : <p className="rounded-sm border border-dashed border-hair-2 px-3 py-2.5 text-[12.5px] text-ink-3">Nearby infrastructure needs a published location for this project.</p>}381          </Section>382383          <Section id="related" title="Related events" className="scroll-mt-28" description="Events attached to this project — announcements, power agreements, financing, status changes — deduplicated across sources.">384            <EventList events={[...p.relatedEvents, ...p.events.filter((e) => !p.relatedEvents.some((x) => x.id === e.id))]} dense emptyTitle="No related event yet" emptyDescription="Announcements, agreements and status changes will list here as connectors detect them." />385          </Section>386387          <Section id="provenance" title="Source evidence" className="scroll-mt-28" description="Every field with its source, observation dates, confidence and extraction method. “Shown” marks the winner backing the displayed value.">388            <ProvenanceTable items={p.provenance} />389            {p.sourceHistory.length > 0 && (390              <details className="mt-4">391                <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">392                  Source observation log <span className="figure text-ink-3">{fmtInt(p.sourceHistory.length)}</span>393                </summary>394                <div className="mt-3">395                  <SourceHistory items={p.sourceHistory} />396                </div>397              </details>398            )}399            <p className="mt-2 text-[11.5px] text-ink-4">400              {CONFIDENCE_LABEL[p.confidence]} overall.{" "}401              <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">402                How confidence is computed403              </Link>404            </p>405          </Section>406407          <Section id="quality" title="Data quality" className="scroll-mt-28">408            <DataQualityPanel q={dq} />409          </Section>410        </div>411412        <SourcesFooter sources={r.sources} updatedAt={p.lastUpdate} />413      </Page>414    </EvidenceProvider>415  );416}417